{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Welcome to Week 4, Day 4\n",
    "\n",
    "This is the start of an AWESOME project! Really simple and very effective."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### First - a heads up for Windows PC users\n",
    "\n",
    "While executing this notebook, you might hit a problem with the Playwright browser raising a NotImplementedError.\n",
    "\n",
    "This should work when we move to python modules, but it can cause problems in Windows in a notebook.\n",
    "\n",
    "If you it this error and would like to run the notebook, you need to make a small change which seems quite hacky!\n",
    "\n",
    "1. Right click in `.venv` in the File Explorer on the left and select \"Find in folder\"\n",
    "2. Search for `asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())`  \n",
    "3. That code should be found in a line of code in a file called `kernelapp.py`\n",
    "4. Comment out the entire else clause that this line is a part of - see the fragment below\n",
    "5. Restart the kernel by pressing the \"Restart\" button above\n",
    "\n",
    "```python\n",
    "        if sys.platform.startswith(\"win\") and sys.version_info >= (3, 8):\n",
    "            import asyncio\n",
    " \n",
    "            try:\n",
    "                from asyncio import WindowsProactorEventLoopPolicy, WindowsSelectorEventLoopPolicy\n",
    "            except ImportError:\n",
    "                pass\n",
    "                # not affected\n",
    "           # else:\n",
    "            #    if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:\n",
    "                    # WindowsProactorEventLoopPolicy is not compatible with tornado 6\n",
    "                    # fallback to the pre-3.8 default of Selector\n",
    "                    # asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())\n",
    "```\n",
    "\n",
    "Thank you to student Nicolas for finding this, and to Kalyan, Yaki, Zibin and Bhaskar for confirming that this worked for them!\n",
    "\n",
    "As an alternative, you can just move to a Python module (which we do anyway in Day 5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph import StateGraph, START\n",
    "from langgraph.graph.message import add_messages\n",
    "from dotenv import load_dotenv\n",
    "from IPython.display import Image, display\n",
    "import gradio as gr\n",
    "from langgraph.prebuilt import ToolNode, tools_condition\n",
    "import requests\n",
    "import os\n",
    "from langchain.agents import Tool\n",
    "\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langgraph.checkpoint.memory import MemorySaver"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Asynchronous LangGraph\n",
    "\n",
    "To run a tool:  \n",
    "Sync: `tool.run(inputs)`  \n",
    "Async: `await tool.arun(inputs)`\n",
    "\n",
    "To invoke the graph:  \n",
    "Sync: `graph.invoke(state)`  \n",
    "Async: `await graph.ainvoke(state)`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class State(TypedDict):\n",
    "    \n",
    "    messages: Annotated[list, add_messages]\n",
    "\n",
    "\n",
    "graph_builder = StateGraph(State)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "pushover_token = os.getenv(\"PUSHOVER_TOKEN\")\n",
    "pushover_user = os.getenv(\"PUSHOVER_USER\")\n",
    "pushover_url = \"https://api.pushover.net/1/messages.json\"\n",
    "\n",
    "def push(text: str):\n",
    "    \"\"\"Send a push notification to the user\"\"\"\n",
    "    requests.post(pushover_url, data = {\"token\": pushover_token, \"user\": pushover_user, \"message\": text})\n",
    "\n",
    "tool_push = Tool(\n",
    "        name=\"send_push_notification\",\n",
    "        func=push,\n",
    "        description=\"useful for when you want to send a push notification\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Next: Install Playwright\n",
    "\n",
    "On Windows and MacOS:  \n",
    "`playwright install`\n",
    "\n",
    "On Linux:  \n",
    "`playwright install —with-reps chromium`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Introducing nest_asyncio\n",
    "\n",
    "Python async code only allows for one \"event loop\" processing aynchronous events.\n",
    "\n",
    "The `nest_asyncio` library patches this, and is used for special situations, if you need to run a nested event loop.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "import nest_asyncio\n",
    "nest_asyncio.apply()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### The LangChain community\n",
    "\n",
    "One of the remarkable things about LangChain is the rich community around it.\n",
    "\n",
    "Check this out:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.agent_toolkits import PlayWrightBrowserToolkit\n",
    "from langchain_community.tools.playwright.utils import create_async_playwright_browser\n",
    "\n",
    "# If you get a NotImplementedError here or later, see the Heads Up at the top of the notebook\n",
    "\n",
    "async_browser =  create_async_playwright_browser(headless=False)  # headful mode\n",
    "toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=async_browser)\n",
    "tools = toolkit.get_tools()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for tool in tools:\n",
    "    print(f\"{tool.name}={tool}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "tool_dict = {tool.name:tool for tool in tools}\n",
    "\n",
    "navigate_tool = tool_dict.get(\"navigate_browser\")\n",
    "extract_text_tool = tool_dict.get(\"extract_text\")\n",
    "\n",
    "    \n",
    "await navigate_tool.arun({\"url\": \"https://www.cnn.com\"})\n",
    "text = await extract_text_tool.arun({})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import textwrap\n",
    "print(textwrap.fill(text))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "all_tools = tools + [tool_push]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "llm_with_tools = llm.bind_tools(all_tools)\n",
    "\n",
    "\n",
    "def chatbot(state: State):\n",
    "    return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "graph_builder = StateGraph(State)\n",
    "graph_builder.add_node(\"chatbot\", chatbot)\n",
    "graph_builder.add_node(\"tools\", ToolNode(tools=all_tools))\n",
    "graph_builder.add_conditional_edges( \"chatbot\", tools_condition, \"tools\")\n",
    "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
    "graph_builder.add_edge(START, \"chatbot\")\n",
    "\n",
    "memory = MemorySaver()\n",
    "graph = graph_builder.compile(checkpointer=memory)\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "config = {\"configurable\": {\"thread_id\": \"10\"}}\n",
    "\n",
    "async def chat(user_input: str, history):\n",
    "    result = await graph.ainvoke({\"messages\": [{\"role\": \"user\", \"content\": user_input}]}, config=config)\n",
    "    return result[\"messages\"][-1].content\n",
    "\n",
    "\n",
    "gr.ChatInterface(chat, type=\"messages\").launch()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
